/**
XenSim simulates xenbal.
Copyright (C) 2006 Travis Vachon

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/
package xensim;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.Vector;
import java.util.Random;



public class Node {
	
	private long CPUSpeed;
	
	protected LinkedList<Domain> domains;
	protected Domain domain0;
	private SimGlobals globals;
	protected Domain currentDomain;
	protected ScheduleEvent nextScheduleEvent;
	
	protected Vector<TransferDaemon> transferDaemons;
	
	public String name;
	public long pageSize;
	public long totalMemory;
	public long freeMemory;
	
	public boolean migrating;
	
	public long migrationReservedMemory;
	
	public double idleTime;
	
	//protected LinkedList<TimeDomainPair> loadList;
	
	private boolean idle;
	private double wentIdleAt;
	
	private Random generator;
	
	
	
	//Connection Bandwidth in bits/ n-second
	//protected long connectionBandwidth;
	
	//protected long usedTransmitBandwidth;
	//protected long usedReceiveBandwidth;
	
	public boolean schedulingPlanned;
	
	protected Connection connection;
	
	/**
	 * 
	 * @param globals
	 * @param name
	 * @param CPUSpeed cpu speed in Mhz
	 * @param memory
	 * @param pageSize
	 * @param connectionBandwidth connection bandwidth in bits/second
	 */
	public Node(SimGlobals globals, 
			String name, 
			int CPUSpeed, 
			long memory, 
			long pageSize, 
			long connectionBandwidth,
			long dom0memoryAllocation,
			RateFn dom0pagerateFn,
			CpuFn dom0workFn,
			NetworkFn dom0networkUsageFn

			){
		this.domains = new LinkedList<Domain>();
		this.transferDaemons = new Vector<TransferDaemon>();
		this.globals = globals;
		this.pageSize = pageSize;
		this.connection = new Connection(globals, connectionBandwidth, domains);
		//this.connectionBandwidth = connectionBandwidth/SimGlobals.TIME_CONSTANT;
		this.totalMemory = memory;
		this.freeMemory = totalMemory;
		this.name = name;
		this.CPUSpeed = CPUSpeed*1000000/SimGlobals.TIME_CONSTANT;
		this.idleTime = 0;
		this.idle = true;
		this.wentIdleAt = 0;
		this.migrationReservedMemory = 0;
		
		//this.loadList = new LinkedList<TimeDomainPair>();
		
		this.generator = new Random(System.identityHashCode(this));
		//this.usedTransmitBandwidth = 0;
		//this.usedReceiveBandwidth = 0;
		
		this.domain0 = new Domain(globals, "dom0", dom0memoryAllocation, dom0pagerateFn, dom0workFn, dom0networkUsageFn);
		this.bootDomain(this.domain0);
	}
	
//
//	}
//	
//	public float getDomainLoad(long interval, Domain domain) throws LoadIntervalTooLargeError{
//		if (interval > SimGlobals.LOAD_MEMORY_TIME){
//			throw new LoadIntervalTooLargeError(interval);
//		}
//		updateLoadList();
//		long queryStartTime = globals.currentTime - interval;
//		long timeOnCPU = 0;
//		
//		for(TimeDomainPair pair: loadList){
//			//Don't look at time before the query start time
//			if (domain == pair.getDomain() && pair.getStopTime() > queryStartTime){
//
//				timeOnCPU += (Math.min(pair.getStopTime(), globals.currentTime)
//						- Math.max(pair.getStartTime(), queryStartTime)); 
//
//			}
//		}
//		return (float)timeOnCPU/interval;
//		
//	}

	/*
	 * Use the parameters in SimGlobals to generate a CPU load for domain 0 from a normal distribution
	 */
	private double dom0CpuUse() {

		double z = generator.nextGaussian();
		
		return Math.max(0.0, Math.min(1.0,z*SimGlobals.DOM0_CPU_USE_STD_DEV + SimGlobals.AVERAGE_DOM0_CPU_USE));
	}
	
	public boolean hasSpace(long memoryFootprint){
		if(memoryFootprint <= freeMemory) return true;
		else return false;
	}
	
	
	
	
	
	public boolean stopDomain(Domain domain) throws StopError{
		if(domain == currentDomain){
			return stopCurrentDomain();
		}
		else if(domains.contains(domain)){
			domain.stop();
			freeMemory += domain.memoryAllocation;
			domains.remove(domain);
			return true;
		}
		else return false;
	}
	
	
	public boolean stopCurrentDomain() throws StopError{
		if(currentDomain != null){

			currentDomain.stop();
			freeMemory += currentDomain.memoryAllocation;
			domains.remove(currentDomain);
			currentDomain = null;
			

			Iterator iterator = globals.eventQueue.iterator();
			while(iterator.hasNext()){
				if(iterator.next() == this.nextScheduleEvent) iterator.remove();
			}
			
			
			if(domains.size() == 0){
				idle = true;
				wentIdleAt = globals.currentTime;
			} else {
				schedule();
			}
			return true;
		}
		else return false;
		
		
	}
		
	
	// Work on: 
	// migrate start/stop
	// reserve space in memory for migrating domains
	//
	//
	public boolean startDomain(Domain domain) throws StartError{
		if(this.hasSpace(domain.memoryAllocation)){
			freeMemory -= domain.memoryAllocation;
			domains.add(domain);
			
			domain.start();
			
			if (numberOfDomains() == 1) {
				schedule();
			}
			return true;
		} 
		else return false;
			
			
	}
	
	/**
	 * Add a domain to this node and start it up
	 * @param domain
	 * @param currentTime
	 * @throws BootError 
	 * @throws  
	 */
	public boolean bootDomain(Domain domain){
		

		if (this.hasSpace(domain.memoryAllocation)) {
			
			try {
				domain.boot(this);
			} catch (BootError e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
						
			try {
				return startDomain(domain);
			} catch (StartError e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				return false;
			}
		} 
		else return false;
		
	}
	
	/**
	 * Picks a domain to run. Returns false if there is no domain available.
	 * 
	 * @return
	 */
	public boolean schedule(){
		Domain firstNewDomain;

		//if(currentDomain != null){
		//	domains.add(currentDomain);
			
			//System.out.println(currentDomain.name + " off " + name + " at " + globals.currentTime);
		//}
		
		currentDomain = domains.poll();
		if(currentDomain != null){
			domains.add(currentDomain);
		}
		firstNewDomain = currentDomain;
		

		long desiredCyclesOnCPU; 
		double timeUntilNextSched = 0;
		
		if(currentDomain != null){
		
			boolean domainPicked = false;
			boolean domainToRun = true;
			do{
				currentDomain.warpedUpdateWork(SimGlobals.DOMAIN_RUN_QUANTUM);

				desiredCyclesOnCPU = currentDomain.workToDo;
				
				if (desiredCyclesOnCPU > 0) { 
					//RETHINK THIS WORK TO DO STUFF!!!!!
					
					// Set time on CPU to the amount of time to do the work for
					// this domain, or run quantum
					long possibleCyclesOnCPU;
					possibleCyclesOnCPU = Math.min(desiredCyclesOnCPU,
							SimGlobals.DOMAIN_RUN_QUANTUM*this.CPUSpeed);
					//System.out.println("W"+desiredCyclesOnCPU);
					//System.out.println("M"+(SimGlobals.DOMAIN_RUN_QUANTUM*this.CPUSpeed));
					
					currentDomain.workToDo -= possibleCyclesOnCPU;
					domainPicked = true;
					
					currentDomain.timeOnCPU += ((double)possibleCyclesOnCPU)/this.CPUSpeed;					
									
					timeUntilNextSched = ((double)possibleCyclesOnCPU)/this.CPUSpeed;
					//System.out.println(currentDomain.name + " on " + name + " at " + globals.currentTime);

				} else {
					
					currentDomain = domains.poll();
					if(currentDomain != null){
						domains.add(currentDomain);
					}
					if(currentDomain == firstNewDomain){
						currentDomain = null;
						domainToRun = false;
					}
				}
			} while (!domainPicked && domainToRun);
			
			if(!domainToRun){
				currentDomain = null;
				idleTime += SimGlobals.IDLE_TIME_QUANTUM;
				timeUntilNextSched = SimGlobals.IDLE_TIME_QUANTUM;
			}
			assert timeUntilNextSched > 0;
			nextScheduleEvent = new ScheduleEvent(globals.currentTime + timeUntilNextSched,
					   this);
					   
			globals.eventQueue.add(nextScheduleEvent);										   
			return true;
			
		} else {
			return false;
		}
	}
	
	

	
	/**
	 * Print out a description of each domain on this node
	 *
	 */
	public void printDomains(){
		for(int i = 0; i < domains.size(); i++){
			System.out.print(i);
			System.out.println(" " + domains.get(i));
		}
	}
	
	/**
	 * 
	 * @param index Index of the domain to migrate
	 * @param destination Destination node for the above domain
	 * 
	 */
	public boolean migrate(Domain migratee, Node destination){
		
		// Make sure the domain is on this node
		if(!domains.contains(migratee)) return false;

		

		//Check if the destination has enough memory to accept this domain		
		long destinationFreeMem = destination.totalMemory;
		for(Domain domain: destination.domains){
			destinationFreeMem -= (domain.memoryPages * destination.pageSize);
		}
		
		if (destinationFreeMem >= migratee.memoryPages*pageSize){
				
			transferDaemons.add(new TransferDaemon(globals,migratee,this,destination));
			return true;
		}else{
			return false;
		}
		
	}
	
	public int numberOfDomains(){
		return domains.size();
		
	}
	
	public static void main(String[] args) {
		
//		SimulationGlobals globals = new SimulationGlobals();
//		
//		Node node = new Node(globals);
//		
//		Domain domain1 = new Domain("Domain 1",
//									64000000,
//									4012,
//									new OneFn(),
//									new OneFn(),
//									new OneFn(),
//									globals
//									);
//		
//		node.bootDomain(domain1);
//		
//		node.printDomains();
	}

	protected class TimeDomainPair{
		private long startTime;
		private long stopTime;
		private Domain domain;
		private TimeDomainPair(Domain domain, long startTime, long stopTime){
			this.startTime = startTime;
			this.stopTime = stopTime;
			this.domain = domain;
			
		}
		protected Domain getDomain(){
			return domain;
			
		}
		protected long getStartTime(){
			return startTime;
		}
		protected long getStopTime(){
			return stopTime;
		}
	}
}
